package com.nulldev.util.data;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.UnaryOperator;

import com.nulldev.util.JVM.Arguments;
import com.nulldev.util.concurrency.threadIt.v4.locking.ResettableCountDownLatch;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;

public abstract class CachedItem<T> extends Thread implements Runnable {
	private static final Logger log = LoggerFactory.getLoggerD(Arguments.hasArgument("--debug", "-debug", "--trace", "--CachedItem.debug"));

	private final Object sync_obj = new Object();
	private final boolean manualMode;
	private final long expiry;
	private final String name;

	private final ResettableCountDownLatch wait_latch = new ResettableCountDownLatch(1);

	private final AtomicReference<T> item = new AtomicReference<T>();

	public CachedItem(final String name, final long expiry) {
		this(name, expiry, false, false);
	}

	public CachedItem(final String name, final long expiry, final boolean manualValueMode) {
		this(name, expiry, manualValueMode, false);
	}

	public CachedItem(final String name, final long expiry, final boolean manualValueMode, final boolean isLowPriority) {
		super(CachedItemThreadGroup.THREAD_GROUP, "CI[" + name + "]");
		this.expiry = expiry;
		this.name = name;
		this.manualMode = manualValueMode;
		super.start();
	}

	public long expiry() {
		return this.expiry;
	}

	@Override
	public synchronized void start() {
		/* NO-OP */
	}

	@Override
	public void run() {
		while (true) {
			log.debug("run::cycle[name=" + this.name + "]");
			this.wait_latch.reset();
			{
				log.debug("run::accquire[" + this.name + "]");
				T object = null;
				try {
					object = this.accquire();
				} catch (Exception e) {
					log.error("Failed to get item!", e);
				} finally {
					this.item.set(object);
				}
				log.debug("run::signal[name=" + this.name + "]");
			}
			try {
				this.wait_latch.countDown();
				if (this.manualMode) {
					log.debug("run::cycle: Disabling main loop due to MANUAL_VALUE_MODE.");
					break;
				} else {
					synchronized (this.sync_obj) {
						this.sync_obj.wait(this.expiry);
					}
				}
			} catch (InterruptedException f) {
				Thread.interrupted();
			}
		}
	}

	public abstract T accquire() throws Exception;

	public T get() {
		return this.item.get();
	}

	public T waitFor() {
		if (this.wait_latch.isCompleted()) {
			return this.item.get();
		} else {
			try {
				this.wait_latch.await();
			} catch (InterruptedException e) {
				Thread.interrupted();
			}
			return this.item.get();
		}
	}

	public T waitTill(final long timeout, final TimeUnit unit) {
		if (this.wait_latch.isCompleted()) {
			return this.item.get();
		} else {
			try {
				if (this.wait_latch.await(timeout, unit))
					throw new TimeoutException("waitTill timed out.");
			} catch (InterruptedException e) {
				Thread.interrupted();
			} catch (TimeoutException e) {
				throw new RuntimeException(e);
			}
			return this.item.get();
		}
	}

	public T reaccquire() {
		if (this.manualMode) {
			/* Unsupported. */
			return this.item.get();
		} else {
			this.item.lazySet(null);
			this.wait_latch.reset();
			synchronized (this.sync_obj) {
				this.sync_obj.notifyAll();
			}
			return this.waitFor();
		}
	}

	public T getAuto() {
		return this.waitFor();
	}

	public boolean hasCachedItem() {
		return this.wait_latch.isCompleted();
	}

	public T set(final T value) {
		if (value == null)
			return null;
		return this.item.updateAndGet(new UnaryOperator<T>() {

			@Override
			public T apply(final T t) {
				return value;
			}
		});
	}
}
